✅Toggl Trackの学習時間をpixelaに自動反映させたい
from Pixela
Toggl Trackの学習時間をpixelaに自動反映させたい
Togglの作業時間をPixelaで可視化する | 高木のブログ
?
LambdaはおそらくAWS Lambdaのこと
GAS
passwordとtokenの管理、よくわからない
毎日0時に前日分のTogglに記録された合計作業時間をPixelaに反映する
Pixelaに草を生やす単位は分
この仕様は私の求めるものと同じ
Toggl APIで前日の総作業時間を取得する | 高木のブログ
Overview | Toggl Engineering
勉強を続ける技術 - nownab.log 2021
TogglとPixelaでデータを連携するためにGoogle CloudのCloud FunctionsとCloud Schedulerを使っています。TogglもPixelaもAPIがあるので簡単に連携できます。 READMEすら書いてませんがソースコードはこちらです。
2025-06-16
わからないのでわからないときにどうするかを実践する場とする
わからないままなんとかなってしまった
2025-06-19
API token
必要なもの
https://engineering.toggl.com/docs/api/me/
2025-06-20
protocolとかTCP/IPって高校の情報とかでも何回か習ったけど毎回意味不明
TCP/IP以外のprotocolの例を知り、おぼろげながら理解が進んできた
具体例の不足がわからなさの原因だったケース
2025-06-29
とりあえず動けばいいのでclaudeをこき使ってなんとかしている
https://claude.ai/chat/98b73a9e-5e80-4074-810a-c5b5271c7877
15:37 できちゃった
github actionsを使った
GitHub web UIでファイル削除する方法ってないのか?
間違って作ったやつがゴミみたいに残ってて気持ち悪い
以下claude 4.iconが作った
claudeいつの間にかweb検索できるようになってる?
code:.github/workflows/sync.yml
name: Toggl to Pixela Sync
on:
schedule:
# 毎日日本時間0:00 (UTC 15:00) に実行
- cron: '0 15 * * *'
workflow_dispatch: # 手動実行も可能
jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run sync script
env:
TOGGL_API_TOKEN: ${{ secrets.TOGGL_API_TOKEN }}
PIXELA_USERNAME: ${{ secrets.PIXELA_USERNAME }}
PIXELA_TOKEN: ${{ secrets.PIXELA_TOKEN }}
PIXELA_GRAPH_ID: ${{ secrets.PIXELA_GRAPH_ID }}
run: python toggl_pixela_sync.py
yml
初めて見た拡張子
code:requrements.txt
requests==2.31.0
python依存関係(何?)に使う
code:toggl_pixela_sync.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Toggl Track to Pixela 自動連携スクリプト (GitHub Actions版)
毎日実行して、プロジェクト名に"📗"が含まれる学習時間をPixelaに送信
"""
import requests
import json
from datetime import datetime, timedelta
import base64
import os
import sys
from typing import List, Dict
def log(message):
"""ログ出力"""
print(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - {message}")
class TogglPixelaSync:
def __init__(self):
# 環境変数から設定を読み込み
self.toggl_api_token = os.getenv('TOGGL_API_TOKEN')
self.pixela_username = os.getenv('PIXELA_USERNAME')
self.pixela_token = os.getenv('PIXELA_TOKEN')
self.pixela_graph_id = os.getenv('PIXELA_GRAPH_ID')
# API設定
self.toggl_base_url = 'https://api.track.toggl.com/api/v9'
self.pixela_base_url = 'https://pixe.la/v1'
# 設定チェック
self._validate_config()
def _validate_config(self):
"""必要な設定がすべて揃っているかチェック"""
required_vars = {
'TOGGL_API_TOKEN': self.toggl_api_token,
'PIXELA_USERNAME': self.pixela_username,
'PIXELA_TOKEN': self.pixela_token,
'PIXELA_GRAPH_ID': self.pixela_graph_id
}
missing_vars = var for var, value in required_vars.items() if not value
if missing_vars:
log(f"エラー: 以下の環境変数が設定されていません: {', '.join(missing_vars)}")
sys.exit(1)
def get_toggl_time_entries(self, target_date: datetime) -> ListDict:
"""指定日のTogglタイムエントリーを取得"""
# 認証ヘッダー
auth_str = f"{self.toggl_api_token}:api_token"
auth_bytes = auth_str.encode('ascii')
auth_b64 = base64.b64encode(auth_bytes).decode('ascii')
headers = {
'Authorization': f'Basic {auth_b64}',
'Content-Type': 'application/json'
}
# 日付範囲設定
start_date = target_date.strftime('%Y-%m-%dT00:00:00+09:00')
end_date = target_date.strftime('%Y-%m-%dT23:59:59+09:00')
log(f"Toggl API params: start_date={start_date}, end_date={end_date}")
url = f"{self.toggl_base_url}/me/time_entries"
params = {
'start_date': start_date,
'end_date': end_date
}
try:
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
entries = response.json()
log(f"Togglから{len(entries)}件のエントリーを取得")
return entries
except requests.exceptions.RequestException as e:
log(f"Toggl API エラー: {e}")
return []
def get_toggl_projects(self) -> Dictint, str:
"""Togglプロジェクト一覧を取得"""
auth_str = f"{self.toggl_api_token}:api_token"
auth_bytes = auth_str.encode('ascii')
auth_b64 = base64.b64encode(auth_bytes).decode('ascii')
headers = {
'Authorization': f'Basic {auth_b64}',
'Content-Type': 'application/json'
}
try:
# ワークスペース取得
workspace_url = f"{self.toggl_base_url}/workspaces"
workspace_response = requests.get(workspace_url, headers=headers)
workspace_response.raise_for_status()
workspaces = workspace_response.json()
if not workspaces:
log("ワークスペースが見つかりません")
return {}
workspace_id = workspaces0'id'
# プロジェクト取得
projects_url = f"{self.toggl_base_url}/workspaces/{workspace_id}/projects"
projects_response = requests.get(projects_url, headers=headers)
projects_response.raise_for_status()
projects = projects_response.json()
project_dict = {}
for project in projects:
project_dict[project'id'] = project'name'
log(f"{len(project_dict)}個のプロジェクトを取得")
return project_dict
except requests.exceptions.RequestException as e:
log(f"Togglプロジェクト取得エラー: {e}")
return {}
def filter_study_entries(self, time_entries: ListDict, projects: Dictint, str) -> ListDict:
"""📗が含まれるプロジェクトのエントリーをフィルタリング"""
study_entries = []
for entry in time_entries:
project_id = entry.get('project_id')
if not project_id:
continue
project_name = projects.get(project_id, '')
if '📗' in project_name:
study_entries.append(entry)
duration_min = round(entry.get('duration', 0) / 60)
log(f"学習エントリー: {project_name} - {duration_min}分")
return study_entries
def calculate_total_minutes(self, time_entries: ListDict) -> int:
"""時間エントリーの合計時間を分単位で計算"""
total_seconds = 0
for entry in time_entries:
duration = entry.get('duration', 0)
if duration > 0: # 進行中でないエントリーのみ
total_seconds += duration
total_minutes = round(total_seconds / 60)
log(f"合計学習時間: {total_minutes}分")
return total_minutes
def post_to_pixela(self, date: datetime, minutes: int) -> bool:
"""Pixelaにデータを送信"""
headers = {
'X-USER-TOKEN': self.pixela_token,
'Content-Type': 'application/json'
}
data = {
'date': date.strftime('%Y%m%d'),
'quantity': str(minutes)
}
url = f"{self.pixela_base_url}/users/{self.pixela_username}/graphs/{self.pixela_graph_id}"
try:
response = requests.post(url, headers=headers, json=data)
response.raise_for_status()
log(f"Pixelaに送信成功: {date.strftime('%Y-%m-%d')} - {minutes}分")
return True
except requests.exceptions.RequestException as e:
log(f"Pixela送信エラー: {e}")
if hasattr(e, 'response') and e.response is not None:
log(f"レスポンス: {e.response.text}")
return False
def sync_yesterday_data(self):
"""昨日のデータを同期"""
from datetime import timezone
# JSTのタイムゾーンを定義
JST = timezone(timedelta(hours=9))
yesterday = datetime.now(JST) - timedelta(days=1)
log(f"データ同期開始: {yesterday.strftime('%Y-%m-%d')}")
# プロジェクト一覧取得
projects = self.get_toggl_projects()
if not projects:
log("プロジェクトが取得できませんでした")
return
# タイムエントリー取得
time_entries = self.get_toggl_time_entries(yesterday)
if not time_entries:
log("該当するタイムエントリーがありませんでした")
return
# 学習エントリーをフィルタリング
study_entries = self.filter_study_entries(time_entries, projects)
# 合計時間計算
total_minutes = self.calculate_total_minutes(study_entries)
# Pixelaに送信
if total_minutes > 0:
success = self.post_to_pixela(yesterday, total_minutes)
if success:
log("同期完了!")
else:
log("同期失敗")
sys.exit(1)
else:
log("学習時間が0分のため、Pixelaには送信しません")
def main():
"""メイン関数"""
log("Toggl to Pixela 同期スクリプト開始")
try:
sync = TogglPixelaSync()
sync.sync_yesterday_data()
except Exception as e:
log(f"エラーが発生しました: {e}")
sys.exit(1)
if __name__ == '__main__':
main()
claudeが出したものは日付範囲指定がyyyy-mm-ddだった
それが原因でtime entryが0件になる予想外の動作をしたので(gptが)修正した
pythonならわかりやすいからギリ読めそう
チュートリアルをさっさと終わらせて足場を作ったほうがよさそうだな